# 2.2 自动求导 PyTorch 中,所有神经网络的核心是 `autograd `包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run )的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。 经过本节的学习,你将收获: - autograd的求导机制 - 梯度的反向传播 ## Autograd简介 `torch.Tensor `是这个包的核心类。如果设置它的属性` .requires_grad` 为 `True`,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用` .backward()`,来自动计算所有的梯度。这个张量的所有梯度将会自动累加到`.grad`属性。 注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。 要阻止一个张量被跟踪历史,可以调用` .detach() `方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 `with torch.no_grad(): `中。在评估模型时特别有用,因为模型可能具有 `requires_grad = True` 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。 还有一个类对于`autograd`的实现非常重要:`Function`。`Tensor `和` Function` 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个` .grad_fn `属性,该属性引用了创建 `Tensor `自身的`Function`(除非这个张量是用户手动创建的,即这个张量的` grad_fn `是 `None` )。下面给出的例子中,张量由用户手动创建,因此grad_fn返回结果是None。 ```python from __future__ import print_function import torch x = torch.randn(3,3,requires_grad=True) print(x.grad_fn) ``` ```python None ``` 如果需要计算导数,可以在 `Tensor` 上调用 `.backward()`。如果` Tensor` 是一个标量(即它包含一个元素的数据),则不需要为 `backward() `指定任何参数,但是如果它有更多的元素,则需要指定一个` gradient `参数,该参数是形状匹配的张量。 创建一个张量并设置`requires_grad=True`用来追踪其计算历史 ```python x = torch.ones(2, 2, requires_grad=True) print(x) ``` ```python tensor([[1., 1.], [1., 1.]], requires_grad=True) ``` 对这个张量做一次运算: ```python y = x**2 print(y) ``` ```python tensor([[1., 1.], [1., 1.]], grad_fn=) ``` `y`是计算的结果,所以它有`grad_fn`属性。 ```python print(y.grad_fn) ``` ```python ``` 对 y 进行更多操作 ```python z = y * y * 3 out = z.mean() print(z, out) ``` ```python tensor([[3., 3.], [3., 3.]], grad_fn=) tensor(3., grad_fn=) ``` `.requires_grad_(...) `原地改变了现有张量的` requires_grad `标志。如果没有指定的话,默认输入的这个标志是` False`。 ```python a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False a = ((a * 3) / (a - 1)) print(a.requires_grad) a.requires_grad_(True) print(a.requires_grad) b = (a * a).sum() print(b.grad_fn) ``` ```python False True ``` ## 2.2.1 梯度 现在开始进行反向传播,因为` out` 是一个标量,因此` out.backward() `和` out.backward(torch.tensor(1.))` 等价。 ```python out.backward() ``` 输出导数` d(out)/dx` ```python print(x.grad) ``` ```python tensor([[3., 3.], [3., 3.]]) ``` 数学上,若有向量函数$\vec{y}=f(\vec{x})$,那么 $\vec{y}$ 关于 $\vec{x}$ 的梯度就是一个雅可比矩阵: $ J=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right) $ 而 `torch.autograd` 这个包就是用来计算一些雅可比矩阵的乘积的。例如,如果 $v$ 是一个标量函数 $l = g(\vec{y})$ 的梯度: $ v=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) $ 由链式法则,我们可以得到: $ v J=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right)=\left(\begin{array}{lll}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right) $ 注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。 ```python # 再来反向传播⼀一次,注意grad是累加的 out2 = x.sum() out2.backward() print(x.grad) out3 = x.sum() x.grad.data.zero_() out3.backward() print(x.grad) ``` ```python tensor([[4., 4.], [4., 4.]]) tensor([[1., 1.], [1., 1.]]) ``` 现在我们来看一个雅可比向量积的例子: ```python x = torch.randn(3, requires_grad=True) print(x) y = x * 2 i = 0 while y.data.norm() < 1000: y = y * 2 i = i + 1 print(y) print(i) ``` ```python tensor([-0.9332, 1.9616, 0.1739], requires_grad=True) tensor([-477.7843, 1004.3264, 89.0424], grad_fn=) 8 ``` 在这种情况下,`y `不再是标量。`torch.autograd` 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 `backward:` ```python v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float) y.backward(v) print(x.grad) ``` ```python tensor([5.1200e+01, 5.1200e+02, 5.1200e-02]) ``` 也可以通过将代码块包装在` with torch.no_grad():` 中,来阻止 autograd 跟踪设置了` .requires_grad=True `的张量的历史记录。 ```python print(x.requires_grad) print((x ** 2).requires_grad) with torch.no_grad(): print((x ** 2).requires_grad) ``` ```python True True False ``` 如果我们想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播), 那么我们可以对 tensor.data 进行操作。 ```python x = torch.ones(1,requires_grad=True) print(x.data) # 还是一个tensor print(x.data.requires_grad) # 但是已经是独立于计算图之外 y = 2 * x x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播 y.backward() print(x) # 更改data的值也会影响tensor的值 print(x.grad) ``` ```python tensor([1.]) False tensor([100.], requires_grad=True) tensor([2.]) ```